孤舟蓑笠翁,独钓寒江雪

Java 并发编程 -- 锁

概述

在并发编程中,多线程同时并发访问的资源叫做临界资源,当多个线程同时访问对象并要求操作相同资源时,分割了原子操作就有可能出现数据的不一致或数据不完整的情况,为避免这种情况的发生,我们会采取同步机制,以确保在某一时刻,方法内只允许有一个线程。那么这种情况下我们就会接触各种类型的锁。本文将对常见的几种类型的锁做一个简要的介绍。

锁的使用

在编程过程中,如果我们要使用锁,可以使用 synchronized 关键字,也可以使用通过实现 Lock 接口来实现锁功能的类,比如:ReentrantLock,ReadWriteLock,ReentrantReadWriteLock等等。
我们为什么要使用锁呢?
锁提供了两种主要特性:

  • 互斥性(mutual exclusion)
  • 可见性(visibility)

互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。
可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

锁的分类

可重入锁

可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然可以获取该锁。可重入锁的一个好处是可一定程度避免死锁。
可重入锁的代表是 ReentrantLocksynchronized
下面通过一个例子来解释一下:

1
2
3
4
5
6
7
8
9
10
public void testSynchronized() {
methodA();
}
private synchronized void methodA() {
Log.e("Test","methodA");
methodB();
}
private synchronized void methodB() {
Log.e("Test","methodB");
}
1
2
15:50:30.559 E/Test: methodA
15:50:30.559 E/Test: methodB

在用 synchronized 标记的方法 methodA 内调用同样用 synchronized 标记的同步方法 methodB,如果不是可重入锁,methodB 将无法执行,造成死锁。

公平锁和非公平锁

公平锁是指多个线程等待同一个锁时,将会安装申请锁的顺序来获得。
非公平锁时指多个线程获取锁的顺序不是按照申请的先后顺序进行的,新的申请锁的线程会有机会进行抢占锁,如果被加入了等待队列后则跟公平锁没有区别。
后面会有专门一篇文章来介绍公平锁和非公平锁的实现原理。
公平锁:new ReentrantLock(true)
非公平锁:new ReentrantLock(false)

独占锁和共享锁

独占锁也可以理解为互斥锁,该锁一次只能被一个线程所持有。比如 ReentrantLocksynchronized
共享锁可以同时被多个线程所持有。比如读写锁 ReadWriteLock 中的读锁。

读写锁

读写锁分为读锁和写锁。在读的时候,上读锁,在写的时候,上写锁。读和读互不影响,读和写互斥,写和写互斥,提高读写文件的效率。
读锁可以理解为共享锁,写锁为独占锁。

内置锁和显式锁

内置锁没有显式获得锁和释放锁操作,比如 synchronized,进入 synchronized 修饰的代码就获得锁,走出相应的代码就释放锁。
显式锁的获得锁和释放锁操作需要显式的进行 lock 以及 unlock 操作。比如 ReentrantLock

乐观素和悲观锁

悲观锁:每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。
我们经常使用的 syncrhoizedReentrantLock 等都属于悲观锁。
乐观锁:每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁。但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁不是锁,是一种策略或者叫概念。

自旋锁

自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。
在JDK1.6中,Java虚拟机提供 -XX:+UseSpinning 参数来启用多线程自旋锁优化,使用 -XX:PreBlockSpin 参数来控制多线程自旋锁优化的自旋次数。
在JDK1.7开始,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整。

阻塞锁

阻塞锁,与自旋锁不同,改变了线程的运行状态。让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。

偏向锁

偏向锁是 JDK1.6 提出来的一种锁优化的机制。其核心的思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,就无需再进行相关的同步操作了,从而节约了操作时间,如果在此之间有其他的线程进行了锁请求,则锁退出偏向模式。在JVM中使用 -XX:+UseBiasedLocking